package net.barbierdereuille.android.AndroBib;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.util.Log;
import android.util.Pair;

public class BibTexParser {
	public enum TokenType
	{
		STRING,
		STRING_VALUE,
		PREAMBLE,
		ENTRY,
		KEY,
		FIELD_NAME,
		FIELD_CONTENT,
		COMMENT,
		ENTRY_END,
		EOF,
		INVALID
	}

	public enum Context {
		GLOBAL,
		ENTRY_KEY,
		FIELD_NAME,
		FIELD_CONTENT,
		STRING_CONTENT,
	}
	
	public enum ItemType {
		PREAMBLE,
		STRING,
		ENTRY
	}
	
	/**
	 * Base class for an element in a bibtex file
	 * @author pbdr
	 */
	public static abstract class BibTexItem {
		abstract public ItemType type();
	}
	
	public static class BibTexEntry extends BibTexItem {
		public BibTexEntry(String t) {
			type = t;
			key = "";
			fields = new LinkedList<Pair<String,String> >();
		}
		@Override
		public ItemType type() { return ItemType.ENTRY; }
		public String type;
		public String key;
		public List<Pair<String,String> > fields;
	}
	
	public static class BibTexString extends BibTexItem {
		public BibTexString(String n, String c) {
			name = n;
			content = c;
		}
		@Override
		public ItemType type() { return ItemType.STRING; }
		public String name;
		public String content;
	}
	
	public static class BibTexPreamble extends BibTexItem {
		public BibTexPreamble(String c) {
			content = c;
		}
		@Override
		public ItemType type() { return ItemType.PREAMBLE; }
		public String content;
	}
	
	public final static String TAG = "BibTexParser";

	private final static String DEFAULT_ENCODING = "US-ASCII";

	private BibTexTokenizer mInput;

	private String mContent;
	private Context mContext;
	private BibTexTokenizer.Type mEndBlock;
	private boolean mIsEndBlock;

	/**
	 * Main constructor
	 * @param input Input stream containing the bibtex file
	 * @throws IOException
	 */
	public BibTexParser(Reader in) throws IOException {
		mIsEndBlock = false;
		mContent = "";
		mContext = Context.GLOBAL;
		mInput = new BibTexTokenizer(in);
	}
	
	/**
	 * Find the encoding in a file, if any
	 * @param f File to examine
	 * @throws IOException 
	 */
	public static String findEncoding(File f) throws IOException {
		final Pattern encoding = Pattern.compile("%\\s*Encoding\\s*:\\s*(\\S+)");
		LineNumberReader s = new LineNumberReader(new InputStreamReader(new FileInputStream(f), DEFAULT_ENCODING));
		// Look for encoding on the 10 first lines only
		for(int i = 0 ; i < 10 ; ++i)
		{
			String line = s.readLine();
			if(line == null)
				return DEFAULT_ENCODING;
			Matcher m = encoding.matcher(line);
			if(m.find())
			{
				String enc = m.group(1).replace('_', '-');
				if(enc == "UTF8") {
					Log.d(TAG, "Using UTF-8 encoding");
					enc = "UTF-8";
				} else if(Charset.isSupported(enc)) {
					Log.d(TAG, "Using " + enc + " envoding");
					return enc;
				}
				return DEFAULT_ENCODING;
			}
		}
		return DEFAULT_ENCODING;
	}
	
	/**
	 * Constructor
	 * @param f File containing the bibtex file
	 */
	public BibTexParser(File f) throws FileNotFoundException, IOException {
		this(new InputStreamReader(new FileInputStream(f), findEncoding(f)));
	}
	
	/**
	 * Constructor
	 * @param content String containing the whole bibtex file
	 */
	public BibTexParser(String content) throws IOException {
		this(new StringReader(content));
	}
	
	/**
	 * Put the input in a recovery state
	 * @return an invalid token
	 * @throws IOException
	 */
	private TokenType invalid() throws IOException {
		mInput.skipToNextArobas();
		mContext = Context.GLOBAL;
		mContent = "";
		return TokenType.INVALID;
	}
	
	/**
	 * Parse the next token
	 * @return the type of the next token
	 */
	public TokenType nextToken() throws IOException {	
		switch(mContext)
		{
		case GLOBAL:
			switch(mInput.nextToken())
			{
			case AROBAS: {
				if(mInput.nextToken() != BibTexTokenizer.Type.TEXT)
				{
					return invalid();
				}
				mContext = Context.ENTRY_KEY;
				mContent = mInput.getText().trim();
				BibTexTokenizer.Type tok = mInput.nextToken();
				if(!BibTexTokenizer.isOpenBlock(tok))
				{
					return invalid();
				}
				mEndBlock = BibTexTokenizer.closingBlock(tok);
				if(mContent.equals("COMMENT")) {
					mInput.skipBlock();
					mContent = mInput.getText();
					return TokenType.COMMENT;
				} else if(mContent.equals("STRING")) {
					mContext = Context.STRING_CONTENT;
					switch(mInput.nextToken()) {
					case TEXT:
						mContent = mInput.getText().trim();
						return TokenType.STRING;
					case EOF:
						return TokenType.EOF;
					default:
						return invalid();
					}
				} else if(mContent.equals("PREAMBLE")) {
					mContext = Context.GLOBAL;
					List<BibTexTokenizer.Type> ends = Arrays.asList(mEndBlock);
					mInput.readUntil(ends);
					mContent = mInput.getText().trim();
					return TokenType.PREAMBLE;
				}
				return TokenType.ENTRY;
			}
			case EOF:
				return TokenType.EOF;
			default:
				mContent = mInput.getText();
				mInput.skipToNextArobas();
				mContent = mContent + mInput.getText();
				return TokenType.COMMENT;
			}
		case STRING_CONTENT:
			mContext = Context.GLOBAL;
			switch(mInput.nextToken()) {
			case EQUAL:
				List<BibTexTokenizer.Type> ends = Arrays.asList(mEndBlock);
				mInput.readUntil(ends);
				mContent = mInput.getText().trim();
				return TokenType.STRING_VALUE;
			case EOF:
				return TokenType.EOF;
			default:
				return invalid();
			}
		case ENTRY_KEY:
			mContext = Context.FIELD_NAME;
			switch(mInput.nextToken()) {
			case TEXT:
				mContent = mInput.getText().trim();
				return TokenType.KEY;
			case COMMA:
				mContent = "";
				return TokenType.KEY;
			case EOF:
				return TokenType.EOF;
			default:
				return invalid();
			}
		case FIELD_NAME:
			if(mIsEndBlock) {
				mIsEndBlock = false;
				mContent = mInput.getText(mEndBlock);
				mContext = Context.GLOBAL;
				return TokenType.ENTRY_END;
			}
			mContext = Context.FIELD_CONTENT;
			switch(mInput.nextToken()) {
			case TEXT:
				mContent = mInput.getText().trim();
				return TokenType.FIELD_NAME;
			case COMMA:
				switch(mInput.nextToken())
				{
				case TEXT:
					mContent = mInput.getText().trim();
					return TokenType.FIELD_NAME;
				default:
					return invalid();
				}
			case CLOSE_BRACE:
				if(mEndBlock == BibTexTokenizer.Type.CLOSE_BRACE) {
					mContext = Context.GLOBAL;
					return TokenType.ENTRY_END;
				}
				return invalid();
			case CLOSE_PAREN:
				if(mEndBlock == BibTexTokenizer.Type.CLOSE_PAREN) {
					mContext = Context.GLOBAL;
					return TokenType.ENTRY_END;
				} else {
					return invalid();
				}
			case EOF:
				return TokenType.EOF;
			default:
				return invalid();
			}
		case FIELD_CONTENT:
			mContext = Context.FIELD_NAME;
			mContent = "";
			switch(mInput.nextToken()) {
			case EQUAL: {
				List<BibTexTokenizer.Type> ends = Arrays.asList(BibTexTokenizer.Type.COMMA, mEndBlock);
				BibTexTokenizer.Type tok = mInput.readUntil(ends);
				mContent = mInput.getText().trim();
				if(tok == mEndBlock) {
					mIsEndBlock = true;
				}
				mContent = mContent.trim();
				return TokenType.FIELD_CONTENT;
			}
			case EOF:
				return TokenType.EOF;
			default:
				return invalid();
			}
		default:
			return invalid();
		}
	}
	
	/**
	 * @return the text corresponding to the current token.
	 */
	public String getText() {
		return mContent;
	}

	public List<BibTexItem> parse() throws IOException {
		List<BibTexItem> result = new LinkedList<BibTexItem>();
		BibTexEntry currentEntry = null;
		String currentField = null;
		TokenType tok = null;
		while(tok != TokenType.EOF) {
			tok = nextToken();
			switch(tok) {
			case ENTRY:
				currentEntry = new BibTexEntry(getText());
				Log.d(TAG, "Parsing entry @" + currentEntry.type);
				break;
			case ENTRY_END:
				Log.d(TAG, "End of entry");
				result.add(currentEntry);
				currentEntry = null;
				break;
			case KEY:
				currentEntry.key = getText();
				break;
			case FIELD_NAME:
				currentField = getText();
				break;
			case FIELD_CONTENT:
				currentEntry.fields.add(new Pair<String,String>(currentField, getText()));
				break;
			case STRING:
				Log.d(TAG, "Parsing string");
				currentField = getText();
				break;
			case STRING_VALUE:
				result.add(new BibTexString(currentField, getText()));
				break;
			case PREAMBLE:
				Log.d(TAG, "Parsing Preamble");
				result.add(new BibTexPreamble(getText()));
				break;
			default:
				break;
			}
		}
		return result;
	}
	
}
